This analysis examines whether changes in federal spending between FY2020 and FY2024:
The analysis is observational and evaluates correlation, not causation.
library(tidyverse)
library(janitor)
library(lubridate)
library(httr)
library(jsonlite)
library(readxl)
library(scales)
library(ggrepel)
library(usmap)
This document uses a single low-ink-to-data style guide for consistent, uncluttered charts.
library(ggplot2)
library(scales)
if (!requireNamespace("bbplot", quietly = TRUE)) {
if (!requireNamespace("remotes", quietly = TRUE)) install.packages("remotes")
remotes::install_github("bbc/bbplot")
}
library(bbplot)
# Strict low-ink theme: no gridlines, no axis lines, small labels
theme_low_ink <- function(base_size = 11) {
bbplot::bbc_style() %+replace% theme(
panel.grid.major = element_blank(),
panel.grid.minor = element_blank(),
panel.grid.major.y = element_blank(),
panel.grid.minor.y = element_blank(),
axis.line = element_blank(),
axis.ticks = element_blank(),
axis.text.y = element_text(size = base_size * 0.6),
axis.text.x = element_text(size = base_size * 0.7),
legend.title = element_blank(),
legend.position = "top",
plot.title.position = "plot",
plot.title = element_text(margin = margin(b = 6)),
plot.subtitle = element_text(margin = margin(b = 8))
)
}
theme_set(theme_low_ink())
# Party color scale (muted, low-ink)
scale_party_fill <- function(...) {
scale_fill_manual(
values = c(
"Biden" = "#2166AC", # muted Dem blue
"Trump" = "#B2182B" # muted GOP red
),
...
)
}
label_billions <- label_dollar(scale = 1e-9, suffix = "B", accuracy = 0.1)
label_dollars1 <- label_dollar(accuracy = 1)
label_pct1 <- label_percent(accuracy = 1)
usaspending_post <- function(endpoint, body) {
res <- POST(
url = paste0("https://api.usaspending.gov", endpoint),
body = body,
encode = "json",
add_headers(`Content-Type` = "application/json")
)
stop_for_status(res)
content(res, as = "parsed", simplifyVector = TRUE)
}
get_state_obligations <- function(fy) {
body <- list(
scope = "place_of_performance",
geo_layer = "state",
filters = list(
time_period = list(list(
start_date = paste0(fy - 1, "-10-01"),
end_date = paste0(fy, "-09-30")
))
)
)
out <- usaspending_post("/api/v2/search/spending_by_geography/", body)
# Robustly normalize the results payload. Depending on httr/content parsing,
# `out$results` may be a data.frame/tibble OR a list of records.
res <- out$results
if (is.null(res) || length(res) == 0) {
return(tibble(state = character(), fiscal_year = integer(), obligations = numeric()))
}
if (is.data.frame(res)) {
return(tibble(
state = toupper(res$shape_code),
fiscal_year = fy,
obligations = res$aggregated_amount
))
}
tibble(
state = toupper(purrr::map_chr(res, "shape_code")),
fiscal_year = fy,
obligations = purrr::map_dbl(res, "aggregated_amount")
)
}
library(tidyverse)
library(janitor)
library(scales)
# Compare baseline vs FY2024 using alpha (no patterns)
# Ensure population table exists (created in the population chunk). If knitting from the middle,
# we'll load it from disk to avoid execution-order issues.
if (!exists("pop")) {
if (file.exists("population_by_state_fy.csv")) {
pop <- readr::read_csv("population_by_state_fy.csv", show_col_types = FALSE) |>
janitor::clean_names() |>
dplyr::transmute(
state = toupper(state),
fiscal_year = as.integer(fiscal_year),
pop = as.numeric(pop)
)
message("Loaded pop from population_by_state_fy.csv")
# Ensure pres2020 exists (created in pres2020 chunk). If knitting from the middle,
# we'll derive it from local election files.
if (!exists("pres2020")) {
if (file.exists("president_2020.csv")) {
pres_long <- readr::read_csv("president_2020.csv", show_col_types = FALSE) |>
janitor::clean_names() |>
dplyr::filter(year == 2020) |>
dplyr::mutate(state = toupper(state_po))
} else if (file.exists("1976-2020-president.csv")) {
pres_long <- readr::read_csv("1976-2020-president.csv", show_col_types = FALSE) |>
janitor::clean_names() |>
dplyr::filter(year == 2020) |>
dplyr::mutate(state = toupper(state_po))
} else {
stop("pres2020 not found and no election file present. Add president_2020.csv (preferred) or 1976-2020-president.csv, or knit from the top.")
}
pres2020 <- pres_long |>
dplyr::group_by(state) |>
dplyr::summarize(
votes_biden = sum(candidatevotes[stringr::str_detect(toupper(candidate), "BIDEN")], na.rm = TRUE),
votes_trump = sum(candidatevotes[stringr::str_detect(toupper(candidate), "TRUMP")], na.rm = TRUE),
biden_share_2p = votes_biden / (votes_biden + votes_trump),
biden_won = votes_biden > votes_trump,
.groups = "drop"
) |>
dplyr::mutate(
winner_2020 = dplyr::if_else(biden_won, "Biden", "Trump"),
winner_2020 = factor(winner_2020, levels = c("Biden", "Trump"))
)
message("Derived pres2020 from local election file.")
}
} else {
stop("Population table `pop` not found. Knit from the top (Run All), or ensure the population chunk runs first to create population_by_state_fy.csv.")
}
}
# Pull spending for FY2017–FY2024 (needed for Pre vs Biden baseline and FY2024 levels)
spending <- purrr::map_dfr(2017:2024, get_state_obligations)
# Sanity check to avoid blank charts
spend_check <- spending |>
count(fiscal_year, name = "n_states") |>
arrange(fiscal_year)
print(spend_check)
## # A tibble: 8 × 2
## fiscal_year n_states
## <int> <int>
## 1 2017 57
## 2 2018 57
## 3 2019 57
## 4 2020 57
## 5 2021 57
## 6 2022 57
## 7 2023 57
## 8 2024 57
if (nrow(spending) == 0) stop("USAspending returned 0 rows. Check connectivity/API response.")
if (any(spend_check$n_states < 40)) warning("Some years returned fewer than 40 states; plots may be incomplete.")
# Merge population for per-capita measures (pop is created in the population chunk)
if (!exists("pop")) stop("Population table `pop` not found. Ensure the population chunk runs before this chunk.")
spending_pc <- spending |>
left_join(pop, by = c("state","fiscal_year")) |>
mutate(oblig_pc = obligations / pop)
# Pre-Biden baseline (FY17–FY20 avg) and Biden era (FY21–FY24 avg)
spend_era <- spending_pc |>
mutate(
era = case_when(
fiscal_year %in% 2017:2020 ~ "Pre (FY17–FY20 avg)",
fiscal_year %in% 2021:2024 ~ "Biden (FY21–FY24 avg)",
TRUE ~ NA_character_
)
) |>
filter(!is.na(era)) |>
group_by(state, era) |>
summarize(
total = mean(obligations, na.rm = TRUE),
pc = mean(oblig_pc, na.rm = TRUE),
.groups = "drop"
) |>
pivot_wider(names_from = era, values_from = c(total, pc)) |>
rename(
pre_total = `total_Pre (FY17–FY20 avg)`,
biden_total = `total_Biden (FY21–FY24 avg)`,
pre_pc = `pc_Pre (FY17–FY20 avg)`,
biden_pc = `pc_Biden (FY21–FY24 avg)`
) |>
mutate(
delta_biden_vs_pre_total = biden_total - pre_total,
delta_biden_vs_pre_pc = biden_pc - pre_pc
)
# FY2024 levels (solid bars)
fy2024 <- spending_pc |>
filter(fiscal_year == 2024) |>
select(state, total_2024 = obligations, pc_2024 = oblig_pc)
# Bar chart data: baseline (striped) vs FY2024 (solid), colored by 2020 winner
if (!exists("pres2020")) stop("pres2020 not found. Ensure the pres2020 chunk runs before the bar charts.")
bars_total <- fy2024 |>
select(state, value = total_2024) |>
mutate(series = "FY2024") |>
bind_rows(
spend_era |>
select(state, value = pre_total) |>
mutate(series = "Pre (FY17–FY20 avg)")
) |>
left_join(pres2020 |> select(state, winner_2020), by = "state") |>
mutate(series = factor(series, levels = c("Pre (FY17–FY20 avg)", "FY2024")))
bars_pc <- fy2024 |>
select(state, value = pc_2024) |>
mutate(series = "FY2024") |>
bind_rows(
spend_era |>
select(state, value = pre_pc) |>
mutate(series = "Pre (FY17–FY20 avg)")
) |>
left_join(pres2020 |> select(state, winner_2020), by = "state") |>
mutate(series = factor(series, levels = c("Pre (FY17–FY20 avg)", "FY2024")))
# Section 12.1 delta dataset
delta_12_1 <- spend_era |>
left_join(pres2020 |> select(state, winner_2020), by = "state")
Provide a CSV named population_by_state_fy.csv with
columns:
statefiscal_yearpoplibrary(tidyverse)
library(janitor)
library(readr)
library(stringr)
# Build accurate state populations for FY2017–FY2024 using Census Population Estimates:
# - 2010–2019 series for 2017–2019
# - 2020–2024 series for 2020–2024
#
# If the source CSVs are not present, this chunk will download them from Census.
POP_OUT <- "population_by_state_fy.csv"
CENSUS_2010S_FILE <- "NST-EST2019-ALLDATA.csv"
CENSUS_2020S_FILE <- "NST-EST2024-ALLDATA.csv"
CENSUS_2010S_URL <- "https://www2.census.gov/programs-surveys/popest/datasets/2010-2019/national/totals/nst-est2019-alldata.csv"
CENSUS_2020S_URL <- "https://www2.census.gov/programs-surveys/popest/datasets/2020-2024/state/totals/NST-EST2024-ALLDATA.csv"
# If you've already created POP_OUT, we use it for speed + reproducibility.
if (file.exists(POP_OUT)) {
pop <- read_csv(POP_OUT, show_col_types = FALSE) |>
clean_names() |>
transmute(
state = toupper(state),
fiscal_year = as.integer(fiscal_year),
pop = as.numeric(pop)
)
} else {
# Download Census source files if missing
if (!file.exists(CENSUS_2010S_FILE)) {
download.file(CENSUS_2010S_URL, destfile = CENSUS_2010S_FILE, mode = "wb", quiet = TRUE)
}
if (!file.exists(CENSUS_2020S_FILE)) {
download.file(CENSUS_2020S_URL, destfile = CENSUS_2020S_FILE, mode = "wb", quiet = TRUE)
}
pop_2010s_raw <- read_csv(CENSUS_2010S_FILE, show_col_types = FALSE) |> clean_names()
pop_2020s_raw <- read_csv(CENSUS_2020S_FILE, show_col_types = FALSE) |> clean_names()
# Keep only state-level rows: SUMLEV == 40 (states), plus DC (also SUMLEV 40 in these files)
# Puerto Rico is included; keep it if you want, but our later charts focus on states + DC.
pop_2010s <- pop_2010s_raw |>
filter(sumlev == 40) |>
select(name, starts_with("popestimate")) |>
mutate(name = toupper(name))
pop_2020s <- pop_2020s_raw |>
filter(sumlev == 40) |>
select(name, starts_with("popestimate")) |>
mutate(name = toupper(name))
# State name -> abbreviation (50 states + DC)
state_lu <- tibble(
name = toupper(state.name),
state = state.abb
) |>
add_row(name = "DISTRICT OF COLUMBIA", state = "DC")
# 2017–2019 from 2010s file; 2020–2024 from 2020s file
pop_2017_2019 <- pop_2010s |>
inner_join(state_lu, by = "name") |>
pivot_longer(
cols = matches("^popestimate(2017|2018|2019)$"),
names_to = "year",
values_to = "pop"
) |>
mutate(
fiscal_year = as.integer(str_extract(year, "\\d{4}")),
pop = as.numeric(pop)
) |>
select(state, fiscal_year, pop)
pop_2020_2024 <- pop_2020s |>
inner_join(state_lu, by = "name") |>
pivot_longer(
cols = matches("^popestimate(2020|2021|2022|2023|2024)$"),
names_to = "year",
values_to = "pop"
) |>
mutate(
fiscal_year = as.integer(str_extract(year, "\\d{4}")),
pop = as.numeric(pop)
) |>
select(state, fiscal_year, pop)
pop <- bind_rows(pop_2017_2019, pop_2020_2024) |>
arrange(state, fiscal_year)
# Save a tidy, analysis-ready file for your zip bundle
write_csv(pop, POP_OUT)
message("Created ", POP_OUT, " using Census sources: ", CENSUS_2010S_FILE, " and ", CENSUS_2020S_FILE)
}
# Quick check: should cover FY2017–FY2024 for 51 entities (50 states + DC)
pop_check <- pop |>
filter(fiscal_year %in% 2017:2024) |>
count(fiscal_year, name = "n_states") |>
arrange(fiscal_year)
print(pop_check)
## # A tibble: 5 × 2
## fiscal_year n_states
## <int> <int>
## 1 2020 51
## 2 2021 51
## 3 2022 51
## 4 2023 51
## 5 2024 51
# OPTION 1: Per-capita delta defined as FY2024 − FY2020 (states + DC only)
valid_states <- c(state.abb, "DC")
# For election-correlation charts, restrict to states + DC
spending_pc_states <- spending_pc |>
mutate(state = toupper(state)) |>
filter(state %in% valid_states)
spending_delta <- spending_pc_states |>
filter(fiscal_year %in% c(2020, 2024)) |>
select(state, fiscal_year, obligations, oblig_pc) |>
pivot_wider(
names_from = fiscal_year,
values_from = c(obligations, oblig_pc),
names_prefix = "fy"
) |>
mutate(
delta_pc = oblig_pc_fy2024 - oblig_pc_fy2020,
delta_total = obligations_fy2024 - obligations_fy2020
)
# Sanity check: should be ~51 rows and delta_pc should be non-missing
print(spending_delta |>
summarize(n_rows = n(), n_delta_pc = sum(!is.na(delta_pc)), n_delta_total = sum(!is.na(delta_total))))
## # A tibble: 1 × 3
## n_rows n_delta_pc n_delta_total
## <int> <int> <int>
## 1 51 51 51
Bars are colored by whether the state’s electoral
votes went to Biden or Trump
in 2020.
Provide a CSV named president_2020.csv in this folder (MIT
Election Lab export recommended) with at least:
yearstate_po (two-letter postal abbreviation)candidatecandidatevotesPRES2020_FILE <- "president_2020.csv"
if (!file.exists(PRES2020_FILE)) {
stop("Missing file: president_2020.csv. Put it in the same folder as this Rmd to color bars by Biden/Trump in 2020.")
}
pres_long <- read_csv(PRES2020_FILE, show_col_types = FALSE) |>
clean_names() |>
filter(year == 2020) |>
mutate(state = toupper(state_po))
pres2020 <- pres_long |>
group_by(state) |>
summarize(
votes_biden = sum(candidatevotes[str_detect(toupper(candidate), "BIDEN")], na.rm = TRUE),
votes_trump = sum(candidatevotes[str_detect(toupper(candidate), "TRUMP")], na.rm = TRUE),
biden_share_2p = votes_biden / (votes_biden + votes_trump),
biden_won = votes_biden > votes_trump,
.groups = "drop"
) |>
mutate(
winner_2020 = if_else(biden_won, "Biden", "Trump"),
winner_2020 = factor(winner_2020, levels = c("Biden", "Trump"))
)
These charts use a horizontal bar layout for readable state labels, and follow the low-ink theme set above.
# Ensure consistent series ordering within each state: Pre above FY2024
pre_lab <- "Pre (FY17–FY20 avg)"
fy24_lab <- "FY2024"
# Order states by the maximum of the two series for readability
state_order_total <- bars_total |>
group_by(state) |>
summarize(max_val = max(value, na.rm = TRUE), .groups = "drop") |>
arrange(max_val) |>
pull(state)
# Force y-axis levels: for each state, Pre then FY2024
y_levels_total <- c(rbind(
paste0(state_order_total, " ", pre_lab),
paste0(state_order_total, " ", fy24_lab)
))
bars_total_plot <- bars_total |>
mutate(
series = factor(series, levels = c(pre_lab, fy24_lab)),
alpha_series = if_else(series == pre_lab, 0.5, 1.0),
state_series = paste0(state, " ", as.character(series)),
state_series = factor(state_series, levels = y_levels_total)
)
ggplot(bars_total_plot, aes(x = value, y = state_series, fill = winner_2020, alpha = alpha_series)) +
geom_col(width = 0.75) +
scale_party_fill() +
scale_alpha_identity(guide = "none") +
scale_y_discrete(labels = function(x) {
st <- sub(" .*", "", x)
ifelse(duplicated(st), "", st)
}) +
scale_x_continuous(labels = label_billions) +
labs(
title = "Federal obligations by state: FY2024 vs pre-Biden baseline",
subtitle = "Top bar per state = mean(FY2017–FY2020); bottom bar = FY2024",
x = "Obligations (billions of $)",
y = NULL
) +
theme_low_ink() +
theme(
panel.grid.major = element_blank(),
panel.grid.minor = element_blank(),
axis.line = element_blank(),
axis.ticks = element_blank(),
axis.text.y = element_text(size = 7)
)
# Ensure consistent series ordering within each state: Pre above FY2024
pre_lab <- "Pre (FY17–FY20 avg)"
fy24_lab <- "FY2024"
# Order states by the maximum of the two series for readability
state_order_pc <- bars_pc |>
group_by(state) |>
summarize(max_val = max(value, na.rm = TRUE), .groups = "drop") |>
arrange(max_val) |>
pull(state)
# Force y-axis levels: for each state, Pre then FY2024
y_levels_pc <- c(rbind(
paste0(state_order_pc, " ", pre_lab),
paste0(state_order_pc, " ", fy24_lab)
))
bars_pc_plot <- bars_pc |>
mutate(
series = factor(series, levels = c(pre_lab, fy24_lab)),
alpha_series = if_else(series == pre_lab, 0.5, 1.0),
state_series = paste0(state, " ", as.character(series)),
state_series = factor(state_series, levels = y_levels_pc)
)
ggplot(bars_pc_plot, aes(x = value, y = state_series, fill = winner_2020, alpha = alpha_series)) +
geom_col(width = 0.75) +
scale_party_fill() +
scale_alpha_identity(guide = "none") +
scale_y_discrete(labels = function(x) {
st <- sub(" .*", "", x)
ifelse(duplicated(st), "", st)
}) +
scale_x_continuous(labels = label_dollars1) +
labs(
title = "Federal obligations per capita by state: FY2024 vs pre-Biden baseline",
subtitle = "Top bar per state = mean(FY2017–FY2020); bottom bar = FY2024",
x = "Obligations per capita ($)",
y = NULL
) +
theme_low_ink() +
theme(
panel.grid.major = element_blank(),
panel.grid.minor = element_blank(),
axis.line = element_blank(),
axis.ticks = element_blank(),
axis.text.y = element_text(size = 7)
)
states_2024 <- read_csv("https://michaelminn.net/tutorials/data/2024-electoral-states.csv") |> clean_names()
districts_2024 <- read_csv("https://michaelminn.net/tutorials/data/2024-electoral-districts.csv") |> clean_names()
pres_2024 <- states_2024 |>
transmute(
state = st,
pres_margin_dem = (votes_dem_2024 - votes_gop_2024) /
(votes_dem_2024 + votes_gop_2024)
)
house_2024 <- districts_2024 |>
group_by(st) |>
summarize(
house_margin_dem =
(sum(votes_dem_2024) - sum(votes_gop_2024)) /
(sum(votes_dem_2024) + sum(votes_gop_2024)),
.groups = "drop"
) |>
rename(state = st)
elections_2024 <- left_join(pres_2024, house_2024, by = "state")
analysis <- spending_delta |>
left_join(elections_2024, by = "state")
ggplot(analysis, aes(delta_pc, pres_margin_dem)) +
geom_hline(yintercept = 0, linewidth = 0.25) +
geom_vline(xintercept = 0, linewidth = 0.25) +
geom_point() +
geom_smooth(method = "lm", se = FALSE) +
scale_x_continuous(labels = dollar) +
scale_y_continuous(labels = percent) +
labs(
title = "Change in Federal Spending vs 2024 Presidential Margin",
x = "Δ Federal Obligations per Capita (FY2024 − FY2020)",
y = "2024 Presidential Margin (Dem − GOP)"
)
What This Chart Shows:
This scatter plot examines whether changes in federal spending per capita between FY2020 and FY2024 correlate with how states voted in the 2024 presidential election. Each point represents one state (or DC), with:
Weak or Non-Existent Linear Relationship: The fitted line’s slope indicates little systematic correlation between changes in federal spending and how states voted in the 2024 presidential election.
Wide Scatter Distribution: States are distributed across all four quadrants, demonstrating that:
No Evidence of Electoral Spending Strategy: The weak correlation suggests federal spending changes did not translate into predictable electoral support in 2024.
Outlier Influence: Individual states that deviate significantly from the trend line represent cases where local political, economic, or demographic factors overwhelmed any spending-related patterns.
ggplot(analysis, aes(delta_pc, house_margin_dem)) +
geom_hline(yintercept = 0, linewidth = 0.25) +
geom_vline(xintercept = 0, linewidth = 0.25) +
geom_point() +
geom_smooth(method = "lm", se = FALSE) +
scale_x_continuous(labels = dollar) +
scale_y_continuous(labels = percent) +
labs(
title = "Change in Federal Spending vs 2024 House Margin",
x = "Δ Federal Obligations per Capita (FY2024 − FY2020)",
y = "2024 House Margin (Dem − GOP)"
)
What This Chart Shows:
This scatter plot tests whether federal spending changes correlate with aggregate House election outcomes at the state level. The analysis uses:
Similar Pattern to Presidential Results: House margins show a comparable relationship to spending changes as presidential results, suggesting federal spending patterns didn’t strongly influence congressional voting either.
More Granular Electoral Data: House margins aggregate many districts within each state, providing a different measure of political sentiment that may be less sensitive to statewide spending changes.
State-Level Aggregation Limitations: Since House races are district-by-district, aggregating to state-level obscures local variations—some districts may have benefited greatly from spending while others didn’t.
Consistency Across Election Types: The similarity between presidential and House scatter patterns reinforces that the spending-voting relationship (or lack thereof) is consistent across different types of federal elections.
Observational Correlation Only: These findings represent correlation, not causation. Federal spending decisions, electoral outcomes, and numerous confounding variables interact in complex ways that this analysis cannot disentangle.
delta_plot <- delta_12_1 |>
mutate(delta = delta_biden_vs_pre_pc) |>
arrange(delta) |>
mutate(state = factor(state, levels = state))
ggplot(delta_plot, aes(x = delta, y = state, fill = winner_2020)) +
geom_col(width = 0.75) +
scale_party_fill(guide = "none") +
scale_x_continuous(labels = label_dollars1) +
labs(
title = "Change in obligations per capita: Biden era vs pre-Biden baseline",
subtitle = "Δ = mean(FY2021–FY2024) − mean(FY2017–FY2020)",
x = "Δ obligations per capita ($)",
y = NULL
) +
theme_low_ink()
What This Chart Shows:
This bar chart compares average federal obligations per capita during the Biden era (FY2021–FY2024) against the pre-Biden baseline (FY2017–FY2020). Bars extending right indicate increased spending; bars extending left indicate decreased spending. Colors represent which candidate won each state in 2020 (blue = Biden, red = Trump).
Mixed Partisan Pattern: Both Trump-won states (red) and Biden-won states (blue) appear across the entire spectrum from large negative to large positive changes, though some notable patterns emerge
ND and DC as Extreme Cases:
Trump States in Both Directions: Republican-won states appear in both positive and negative change categories. If partisan favoritism were systematic, we would expect most or all Republican states clustered in the negative range—but this is not the case.
Biden States Also Mixed: Democratic-won states similarly show both increases and decreases, suggesting that 2020 electoral outcomes alone do not predict spending changes.
Energy State Decline: ND’s massive decline likely reflects sectoral policy shifts—the Biden administration’s movement away from fossil fuel development would disproportionately impact major oil and gas producing states.
Policy Over Politics: The distribution pattern suggests federal spending changes were driven primarily by:
Challenges Simple Narratives: This chart provides evidence against claims that the Biden administration systematically “rewarded” states that voted Democratic in 2020—the data show no clear partisan pattern.
Complex Causality: The wide variation across states with different political alignments suggests federal spending patterns result from complex interactions of policy priorities, economic needs, programmatic structures, and institutional factors—not simple political calculations.
Observational Analysis: These findings represent observed correlations between spending changes and political characteristics. Causation cannot be inferred—we cannot determine whether political factors influenced spending, whether spending influenced politics, or whether both were driven by other factors.
You have two recommended options:
Use officer + rvg to insert charts as
editable vectors:
library(officer)
library(rvg)
ppt <- read_pptx()
ppt <- add_slide(ppt, layout = "Title and Content", master = "Office Theme")
ppt <- ph_with(
ppt,
dml(ggobj = last_plot()),
location = ph_location_type(type = "body")
)
print(ppt, target = "spending_elections_2020_2024.pptx")
This preserves full resolution and allows text editing directly in PowerPoint.